ATOM Documentation

← Back to App

Email Service Standard

Overview

The email service follows a class-based pattern for consistency and maintainability. All email operations MUST use the EmailService class from @/lib/email.

Implementation

Email Service Class

**Location**: src/lib/email.ts

**Pattern**: Class-based with async methods

import { emailService } from '@/lib/email'

// ✅ GOOD: Using emailService class
await emailService.sendVerificationEmail(email, name, token)
await emailService.sendWelcomeEmail(email, name, subdomain)
await emailService.sendUpgradeConfirmation(email, planId)
await emailService.sendSupportTicketUpdateEmail(email, ticketId, status)

Email Service Interface

interface SendEmailResult {
  success: boolean
  messageId?: string
  error?: string
}

class EmailService {
  async sendVerificationEmail(
    email: string,
    name: string,
    token: string
  ): Promise<SendEmailResult>

  async sendWelcomeEmail(
    email: string,
    name: string,
    subdomain?: string,
    password?: string
  ): Promise<SendEmailResult>

  async sendUpgradeConfirmation(
    email: string,
    planId: string
  ): Promise<SendEmailResult>

  async sendSupportTicketUpdateEmail(
    email: string,
    ticketId: string,
    status: string
  ): Promise<SendEmailResult>

  // Private method for sending emails
  private async sendMail(
    options: nodemailer.SendMailOptions
  ): Promise<SendEmailResult>
}

Usage Patterns

Standard Usage

import { emailService } from '@/lib/email'

// Send verification email
const result = await emailService.sendVerificationEmail(
  'user@example.com',
  'John Doe',
  'verification-token-123'
)

if (!result.success) {
  console.error('Failed to send email:', result.error)
}

Error Handling

const result = await emailService.sendWelcomeEmail(
  email,
  name,
  subdomain,
  password
)

if (!result.success) {
  // Log error but don't block user flow
  console.error('Welcome email failed:', result.error)
  // Continue with user registration
}

Async/Await

All email methods are async and return Promise<SendEmailResult>:

// ✅ GOOD: Await the result
const result = await emailService.sendVerificationEmail(...)

// ❌ BAD: Don't fire and forget (unless intentional)
emailService.sendVerificationEmail(...) // No await!

Transport Configuration

The email service automatically selects the best transporter:

  1. **AWS SES** (Primary): Uses AWS SES if credentials are available
  2. **SMTP** (Fallback): Uses SMTP if SES is not configured

Configuration Priority

// AWS SES Configuration
const sesConfig = {
  accessKeyId: process.env.SES_AWS_ACCESS_KEY_ID || process.env.AWS_ACCESS_KEY_ID,
  secretAccessKey: process.env.SES_AWS_SECRET_ACCESS_KEY || process.env.AWS_SECRET_ACCESS_KEY,
  region: process.env.AWS_REGION || 'us-east-1'
}

// SMTP Fallback Configuration
const smtpConfig = {
  host: process.env.EMAIL_HOST,
  port: process.env.EMAIL_PORT || '587',
  secure: process.env.EMAIL_SECURE === 'true',
  auth: {
    user: process.env.EMAIL_USER,
    pass: process.env.EMAIL_PASSWORD
  }
}

Best Practices

1. Always Use the Class

// ✅ GOOD: Using emailService class
import { emailService } from '@/lib/email'
await emailService.sendWelcomeEmail(email, name)

// ❌ BAD: Direct nodemailer usage
import nodemailer from 'nodemailer'
const transporter = nodemailer.createTransport(...)
await transporter.sendMail(...) // Don't do this

2. Handle Errors Gracefully

// ✅ GOOD: Handle errors without blocking
const result = await emailService.sendVerificationEmail(...)
if (!result.success) {
  console.error('Email failed:', result.error)
  // Continue with user flow
}

// ❌ BAD: Block user flow on email failure
if (!result.success) {
  throw new Error('Failed to send email') // Don't block users!
}

3. Use Descriptive Subjects

// ✅ GOOD: Clear subject
await emailService.sendMail({
  subject: 'Welcome to ATOM - Your Account is Ready',
  html: '...'
})

// ❌ BAD: Vague subject
await emailService.sendMail({
  subject: 'Email',
  html: '...'
})

4. Include HTML and Text

// ✅ GOOD: Both HTML and text
await emailService.sendMail({
  html: '<h1>Welcome</h1>',
  text: 'Welcome'
})

// ⚠️ ACCEPTABLE: HTML only (with fallback)
await emailService.sendMail({
  html: '<h1>Welcome</h1>'
})

Migration Guide

Current State

✅ **Already Compliant**: The email service is already implemented as a class with no standalone functions.

No migration needed! All code should use the emailService class instance.

Usage

// Import the singleton instance
import { emailService } from '@/lib/email'

// Use it in your code
await emailService.sendVerificationEmail(email, name, token)

Testing

Mock Email Service

// In tests, mock the email service
jest.mock('@/lib/email', () => ({
  emailService: {
    sendVerificationEmail: jest.fn().mockResolvedValue({
      success: true,
      messageId: 'test-message-id'
    })
  }
}))

Test Email Sending

test('sends verification email', async () => {
  const result = await emailService.sendVerificationEmail(
    'test@example.com',
    'Test User',
    'token-123'
  )

  expect(result.success).toBe(true)
  expect(result.messageId).toBeDefined()
})

Monitoring

Track Email Metrics

  • Delivery success rate
  • Delivery failures by type
  • Message IDs for tracking
  • Error rates by provider

Logging

The email service logs:

  • Successful sends with message ID
  • Failed sends with error details
  • Transporter selection (SES vs SMTP)

Security

Environment Variables

Required environment variables:

# AWS SES (Primary)
SES_AWS_ACCESS_KEY_ID=xxx
SES_AWS_SECRET_ACCESS_KEY=xxx
AWS_REGION=us-east-1

# SMTP Fallback
EMAIL_HOST=smtp.example.com
EMAIL_PORT=587
EMAIL_SECURE=false
EMAIL_USER=user@example.com
EMAIL_PASSWORD=password

# From Address
EMAIL_FROM=noreply@atom.ai
SES_SENDER_EMAIL=noreply@atom.ai

Don't Expose Credentials

// ✅ GOOD: Use environment variables
const transporter = nodemailer.createTransport({
  auth: {
    user: process.env.EMAIL_USER,
    pass: process.env.EMAIL_PASSWORD
  }
})

// ❌ BAD: Hardcoded credentials
const transporter = nodemailer.createTransport({
  auth: {
    user: 'user@example.com', // NEVER hardcode!
    pass: 'password'          // NEVER hardcode!
  }
})

References

  • Implementation: src/lib/email.ts
  • Nodemailer Docs: https://nodemailer.com/
  • AWS SES Docs: https://docs.aws.amazon.com/ses/

Changelog

  • 2026-02-08: Standard documented (already implemented as class)
  • 2026-02-08: No migration needed - already compliant